---
id: 测试
title: "前端测试"
---

## 单元测试

### 常见测试框架的几大功能及区别

|    功能    | Jest |                           Mocha                            |
| :--------: | :--: | :--------------------------------------------------------: |
|    断言    | 内置 | 需要另外安装 [Chai](https://www.chaijs.com) 或者其他断言库 |
|  异步支持  | 内置 |                            内置                            |
|    Mock    | 内置 |      Mocha 需要另外安装 [Sinon](https://sinonjs.org/)      |
| 代码覆盖率 | 内置 |   Mocal 需要另外安装 [istanbul](https:/istanbuljs.orgl)    |

### jest 常用 API

|         API Name         |                          用途                           |
| :----------------------: | :-----------------------------------------------------: |
|         describe         |          将几个相关的测试放到一个组中，非必须           |
|     test :(别名 it)      |               测试用例，是测试的最小单位                |
|          expect          | 提供很多的 matcher 来判定你的方法返回值是否符合特定条件 |
|          toBe()          |                     绝对相等（===）                     |
|        toEqual()         |        简单类型绝对匹配；复杂类型内容结果的匹配         |
|        toBeNull()        |                     判断是否为 null                     |
|     toBeUndefined()      |                     匹配 undefined                      |
|      toBeDefined()       |                    匹配非 undefined                     |
|       toBeTruthy()       |                    匹配转化后为 true                    |
|       toBeFalsy()        |                   匹配转化后为 false                    |
|    toBeGreaterThan()     |                      相当于大于号                       |
|      toBeLessThan()      |                      相当于小于号                       |
| toBeGreaterThanOrEqual() |                    相当于大于等于号                     |
|  toBeLessThanOrEqual()   |                    相当于大于等于号                     |
|      toBeCloseTo()       |                    解决 js 浮点错误                     |
|  toMatch(regExp/string)  |          用正则表达式或者字符串匹配字符串片段           |
|       toContain()        |               匹配数组或者 Set 中的某一项               |
|        toThrow()         |        匹配异常处理，如果抛出了异常就过测试用例         |
|         toBeNaN          |                     判断是否为 NaN                      |
|       toHaveLength       |                  数组用，检测数组长度                   |

### Jest 命令的测试范围

- xxx.spec.js
- xxx.test.js
- 放在 \_\_tests\_\_文件夹下

### 小试牛刀

- 准备工作

  - `npm init -y`
  - `npm i jest -D`
  - package.json 添加`"test": "jest"`命令
  - 新建一个 index.test.js 文件

- 简单演示

```js
// index.test.js
function sum(a, b) {
  return a + b;
}

test("test sum", () => {
  expect(sum(1, 4)).toBe(3);
});

test("test", () => {
  const n1 = 0.1;
  const n2 = 0.2;
  // expect(n1+n2).toBe(0.3);
  // expect(n1+n2).toEqual(0.3);
  // expect(n1+n2).toBeCloseTo(0.3); // 接近 0.3
});

test("test null", () => {
  let n = null;
  expect(n).toBeNull();
});

test("test undefined", () => {
  let undef = undefined;
  expect(n).toBeUndefined();
});

test("test toBeDefined 匹配器", () => {
  const a = undefined;
  expect(a).toBeDefined();
});

test("test bool", () => {
  const t = true;
  const f = false;
  expect(t).toBeTruthy();
  expect(f).toBeFalsy();
});

test("test 数字大小", () => {
  const num = 2;
  expect(num).toBeGreaterThan(1); // 判断 num > 1
  expect(num).toBeLessThan(3); // 判断 num < 3
  expect(num).toBeGreaterThanOrEqual(2); // 判断 num 大于或等于
  expect(num).toBeLessThanOrEqual(1); // 判断 num 小于或等于
});

test("test match", () => {
  const str = "hello zhangsan";
  expect(str).toMatch("zhangsan"); // 是否包含字符串
});

test("test contain", () => {
  const str = ["hello", "zhangsan"];
  expect(str).toContain("zhangsan"); // 是否包含某一项
});

function testError(params) {
  // return ''
  throw new Error("this is a error");
}

test("test bool", () => {
  expect(testError).toThrow();
  expect(testError).toThrow("xxx");
});
```

- Promise 测试  
  `Jest会等待Promise的resove状态 如果 Promise 的状态变为 rejected, 测试将会失败。`

```js
const userInfo = () => {
  return new Promise((r, j) => {
    r("hello1");
  });
};

it("test promise", () => {
  userInfo().then((data) => {
    expect(data).toBe("hello");
  });
});
```

```js
const userInfo = (type) => {
  return new Promise((r, j) => (type ? r("success") : j("error")));
};

it("test expect resolves", () => {
  expect(userInfo(true)).resolves.toBe("success");
});

it("test expect rejects", () => {
  return expect(userInfo(false)).rejects.toBe("error");
});
```

# async/await

```js
const userInfo = () => {
  return new Promise((r, j) => {
    r("hello");
  });
};

it("test async", async () => {
  const data = await userInfo();
  expect(data).toBe("hello");
});
```

- 测试异步回调

```js
// index.test.js  错误演示
function getUserInfo(cb) {
  setTimeout(() => {
    cb("hello");
  }, 5000);
}

it("test cb", () => {
  getUserInfo((data) => {
    expect(data).toBe("hello");
  });
});
```

`可以看到回调明明是5s后，但是我们的case 完成时间却在5s 之前，在这里 同步代码getUserInfo执行结束，此case就结束了 `

```js
// 正确演示
function getUserInfo(cb) {
  setTimeout(() => {
    cb("hello");
  }, 5000);
}

it("test cb", (done) => {
  getUserInfo((data) => {
    expect(data).toBe("hello");
    done(); //告诉用例只有done执行才算完成
  });
});
```

- 控制时间的方法

```js
// 例子1
const getUserInfo = (cb) => {
  setTimeout(() => {
    cb("success");
  }, 1000);
};
jest.useFakeTimers(); // 指定 Jest 使用假的全局日期、性能、时间和定时器 API。 假定时器实现由 @sinonjs/fake-timaters 支持。
it("test cb timer", () => {
  const cb = jest.fn();
  getUserInfo(cb);
  // expect(cb).not.toHaveBeenCalled();
  // expect(setTimeout).toHaveBeenCalledTimes(1);
  jest.runAllTimers(); // 运行完所有的定时器
  expect(cb).toHaveBeenCalledTimes(1); // 确保模拟函数被调用的确切次数
  expect(cb).toHaveBeenCalledWith("success"); // 确保使用特定参数调用模拟函数
});

// 例子2  精确控制时间
const getUserInfo2 = (cb1, cb2) => {
  setTimeout(() => {
    cb1("cb1");

    setTimeout(() => {
      cb2("cb2");
    }, 2000);
  }, 1000);
};
jest.useFakeTimers();
it("test cb timer2", () => {
  const cb1 = jest.fn();
  const cb2 = jest.fn();
  getUserInfo2(cb1, cb2);
  // expect(cb1).not.toHaveBeenCalled();
  // expect(setTimeout).toHaveBeenCalledTimes(1);
  jest.runOnlyPendingTimers(); // 仅执行当前挂起的宏任务
  expect(cb1).toHaveBeenCalledTimes(1);
  expect(cb1).toHaveBeenLastCalledWith("cb1");

  jest.runOnlyPendingTimers();
  expect(cb2).toHaveBeenCalledTimes(1);
});
```

```js
// 例子3  自定义前进时间

const getUserInfo2 = (cb1, cb2) => {
  setTimeout(() => {
    cb1("cb1");

    setTimeout(() => {
      cb2("cb2");
    }, 2000);
  }, 1000);
};

jest.useFakeTimers();

it("test cb timer2", () => {
  const cb1 = jest.fn();
  const cb2 = jest.fn();
  getUserInfo2(cb1, cb2);
  // expect(cb1).not.toHaveBeenCalled();
  jest.advanceTimersByTime(500); // 前进500ms
  expect(cb1).toHaveBeenCalledTimes(1);
  expect(cb1).toHaveBeenLastCalledWith("cb1");
  jest.advanceTimersByTime(500); // 前进500ms
  expect(cb2).toHaveBeenCalledTimes(1);
});
```

### 模式介绍

:::tip
jest --watchAll 命令可以监视文件所有更改自动测试  
jest --watch 命令可以监视文件更改自动测试，默认就是 o 模式  
jest --debug 打印关于 Jest 配置的调试信息。

:::

| jest 模式 |                   介绍                    |                                                                       备注                                                                        |
| :-------: | :---------------------------------------: | :-----------------------------------------------------------------------------------------------------------------------------------------------: |
|  f 模式   | Press f to quit "only failed tests" mode. |                              第一次全运行并记录错误的 case，再次执行仅执行记录中错误的 case，再执行又是全运行，…………                               |
|  o 模式   | only run tests related to changed files.  | 仅运行在 hg/git 上有改动但尚未提交的文件，依赖 git 的 diff 算法(依赖 git)，先把项目托管到 git 上。--watch 默认就是 o 模式, 也可以直接运行 jest -o |
|  t 模式   |  to filter by a test name regex pattern.  |                                                      （主要是匹配 describe 或 test 的名称）                                                       |
|  p 模式   |  to filter by a filename regex pattern.   |                                                                   匹配文件名字                                                                    |

<!-- 我们在关联本地与远程时，两端都是有内容的，但是这两份内容并没有联系，当我们推送到远程或者从远程拉取内容时，都会有没有被跟踪的内容，于是你看git报的详细错误中总是会让你先拉取再推送，但是拉取总是失败 -->

- 关联 git 项目。

  - gitee 上创建一个项目。
  - 本地项目根目录 git init。
  - 执行命令：` git remote add origin git@giteexxxxxxx`
  - 提交文件 `git add .` 查看提交内容：`git status` commin ` git commit -m 'xxx'` 推到服务器 `git push`
  - 提示这个就执行，没有就略过 `git push --set-upstream origin master`
  - 提示 failed to push some refs to 'gitee.com:big_hui/unit.git' 就执行 `git pull --rebase origin master`

### Jest 实现 Mock

mock 的主要作用是替代前端请求中的异步数据，让测试代码和真实代码解耦，优点是真实代码修改后，相关 mock 的测试代码不用再修改。

- Mock 的两大功能

  - 创建 mock function，在测试中使用，用来测试回调
  - 手动 mock，覆盖第三方实现。

- 创建 mock

```js
function mockFn(isCall, cb) {
  if (isCall) {
    cb(99);
  }
}

it("test mock function", () => {
  const mockCB = jest.fn(); // 生成假的回调
  mockFn(true, mockCB);
  expect(mockCB).toHaveBeenCalled(); //回调是否被调用
  expect(mockCB).toHaveBeenCalledWith(99); //回调参数是否是9
  expect(mockCB).toHaveBeenCalledTimes(1); //回调执行了几次
  console.log(mockCB.mock.calls);
});

it("test mock implementation", () => {
  // const mockCB = jest.fn(x => x - 3); // 生成假的回调
  const mockCB = jest.fn().mockReturnValue(96); // 生成假的回调
  // mockFn(true, mockCB);
  // expect(mockCB).toHaveBeenCalledWith(99); //回调参数是否是9
  // expect(mockCB).toHaveBeenCalledTimes(1); //回调执行了几次
  console.log(mockCB.mock.results);
});
```

- 手动 mock

```js
const axios = require("axios");

const getUserInfo = () => {
  return axios.get("url");
};

jest.mock("axios");
// axios.get.mockReturnValue(Promise.resolve({name:'李四'}))
// axios.get.mockResolvedValue({name:'王二'})

axios.get.mockImplementation(() => {
  return new Promise((r, j) => {
    r({ name: "张三" });
  });
});

it("test axios", () => {
  getUserInfo().then((data) => {
    console.log(data);
  });
});
```

每次测试文件都需要 mock axios 比较麻烦，我们可以在根目录新建个\_\_mocks\_\_文件夹，  
再新建一个和包名相同名称的文件。

```js
// __mocks__/axios.js
const axios = {
  get: jest.fn(() => Promise.resolve({ name: "麻子" })),
};

module.exports = axios;
```

```js
// test.js
const axios = require("axios");

const getUserInfo = () => {
  return axios.get("url");
};

it("test axios", () => {
  getUserInfo().then((data) => {
    console.log(data);
  });
});
```

- mock 方法和 处理

  - Jest 的 mock 方式 （Jest.fn()、Jest.spyOn()、Jest.mock()）
  - 预处理和后处理
    - beforeAll / afterAll ： 对测试文件中所有的用例开始前/ 后 进行统一的预处理
    - beforeEach/ afterEach : 在每个用例开始前 / 后 进行预处理

### vue 组件测试

新建 vue 项目并安装以下插件

```js
npm init vite@latest my-vue-app -- --template vue-ts
```

```js
vue add unit-jest
// npm i @vue/cli-plugin-unit-jest
```

这个插件会安装以下几个插件

- 注册新命令 "test:unit": "vue-cli-service test:unit",
- @vue/test-utils
- @vue/vue3-jest(将 SFC 文件转换成对应的 js 文件，不然 jest 不认识 vue 文件)
- babel-jest
- jest

会生成 jest.config.js 文件内容如下

```js
module.exports = {
  preset: "@vue/cli-plugin-unit-jest/presets/no-babel",
};
```

:::warning 1.小坑：package.json 内有个[type:module](https://nodejs.org/docs/latest-v13.x/api/esm.html#esm_enabling)  
目前 node 默认的是如果 pacakage.json 没有定义 type 字段，则按照 commonJs 规范处理
不管 type 字段的值是多少, 以.mjs 后缀名的文件总是被当作 ES6 模块,而以.cjs 后缀名的文件总是被当成 CommonJS 模块。

2.大坑-报错模糊坑：如果提示找不到 ts-jest 就去安装了 ts-jest 发现还是一样的。 请注意下面的包版本要保持一致

- vue3-jest
- babel-jest
- jest
- ts-jest

例如我的版本：

- "@vue/vue3-jest": "^27.0.0-alpha.1",
- "babel-jest": "^27.0.6",
- "jest": "^27.0.5",
- "ts-jest": "^27.1.5"

- 如果 报错是 testMatch: **/tests/unit/**/_.spec.[jt]s?(x), \*\*/**tests**/_.[jt]s?(x) - 0 matches
  是不识别 ts 文件。 加如以下配置

```js
moduleFileExtensions: ["js", "jsx", "json", "vue"];
```

- 如果报以下错误：jest SyntaxError: Cannot use import statement outside a module  
  是 jest 不支持 esm。解决办法进行转换操作，在 jest.config.js 中加入以下配置

```js
    transform: {
        // transform files with ts-jest
        "^.+\\.(js|ts)$": "ts-jest",
    },
    globals: {
    "ts-jest": {
        tsConfig: {
        // allow js in typescript
        allowJs: true,
        },
    },
    },

```

:::
运行时如果缺少 @babel/preset-env

经验：如果遇到报错提示无作用（比较模糊时），解决不了的时候。尝试多搜索一些配置的方法，不局限在报错上面。

```
install  @babel/preset-env -D
```

#### Vue Test Utils 常用的方法

|     API Name     |                                        用途                                         |
| :--------------: | :---------------------------------------------------------------------------------: |
|   shallowMount   | 创建一个包含被挂载和渲染的 Vue 组件的 Wrapper，只挂载一个组件而不渲染其子组件(推荐) |
|      mount       |                        创建一个包含被挂载和渲染的 Vue 组件的                        |
|    Wrapper.vm    |           Vue 实例。你可以通过 wrapper.vm 访问一个实例所有的方法和属性。            |
| Wrapper.classes  |                     返回是否拥有该 class 的 dom 或者类名数组。                      |
|   Wrapper.find   |                             返回第一个满足条件的 dom。                              |
| Wrapper.findAll  |                              返回所有满足条件的 dom。                               |
|   Wrapper.html   |                                 返回 html 字符串。                                  |
|   Wrapper.text   |                                  返回内容字符串。                                   |
| Wrapper.setData  |                            设置该组件的初始 data 数据。                             |
| Wrapper.setProps |              设置该组件的初始 props 数据。 （这是使用了，但没有效果）               |
| Wrapper.trigger  |                                   用来触发事件。                                    |
|                  |                                                                                     |

:::tip
推荐使用 shallowMount。单元测试的重点在"单元"二字，想测试子组件再为子组件写对应的测试代码即可。
:::

- 元素是否显示
- get getAll
- find findAll
- getComponent findComponent

get 和 find 的区别  
get 在找不到元素是会报错，find 则不会报错

```js
// HelloWold.vue
<template>
  <h1>{{ msg }}</h1>
  <button @click="setCount">{{count}}</button>
  <test-Aaa />
  <input type="text" v-model="todo">
  <button class="addTodo" @click="addTodo">add</button>
  <ul>
    <li v-for="(item, index) of todoList" :key="index">{{item}}</li>
  </ul>
</template>
<script lang="ts">
import { ref, defineComponent, Ref, defineProps, Prop } from 'vue'
export default defineComponent({
  props: ['msg'],
  emits: ['send'],
  components: {
  },
  setup(props:any, ctx: any){
    let count = ref(0);
    let todo = ref('');
    let todoList:Ref<string[]> = ref([]);
    return {
      count,
      todo,
      todoList,
      setCount(){
        count.value++
      },
      addTodo(){
        todoList.value.push(todo.value);
        ctx.emit('send', todo.value)
      }
    }
  }
})
</script>

<style scoped>
.read-the-docs {
  color: #888;
}
</style>

```

```js
// 测试文件
import { shallowMount, mount } from "@vue/test-utils";
import HelloWorld from "@/components/HelloWorld.vue";

describe("HelloWorld.vue", () => {
  it("renders props.msg when passed", async () => {
    const msg = "new message";
    // const wrapper = shallowMount(HelloWorld, {
    //   props: { msg }
    // });

    const wrapper = mount(HelloWorld, {
      props: { msg },
    });
    expect(wrapper.text()).toMatch(msg);
    console.log(wrapper.html());
    console.log(wrapper.find("h1").text());
    console.log(wrapper.get("h1").text());
    console.log(wrapper.findComponent(HelloWorld).props());

    const btn = wrapper.get("button");
    console.log(btn.text());
    await btn.trigger("click");

    expect(btn.text()).toBe("1");
    // expect(3).toBe(3)
  });

  it("renders todo list", async () => {
    const msg = "new message";
    const wrapper = shallowMount(HelloWorld, {
      props: { msg },
    });

    const input = wrapper.get("input");
    await input.setValue("111");
    expect(input.element.value).toBe("111");
    await wrapper.get(".addTodo").trigger("click");
    expect(wrapper.findAll("li")).toHaveLength(1);
    expect(wrapper.get("li").text()).toBe("111");

    console.log(wrapper.emitted());
    expect(wrapper.emitted()).toHaveProperty("send");
    const params = wrapper.emitted("send");
    console.log("wrapper.emitted()", params);
    expect(params[0]).toEqual(["111"]);
  });
});
```

```js
// jest.config.js // jest支持的文件类型
  moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'],
```

- 异步测试
  使用 flushpromises 将所有 Promise p

```js
// HelloWold.vue

<template>
  <button @click="getUserInfo">查询</button>
  <lu>
    <li v-if="status === 0" class="loading">loading</li>
    <li v-else-if="status === 1" class="success">success</li>
    <li v-else class="error">error</li>
  </lu>
</template>
<script lang="ts">
import { ref, defineComponent, Ref, defineProps, Prop, onMounted } from 'vue'
import axios from 'axios';
export default defineComponent({
  props: ['msg'],
  emits: ['send'],
  components: {
  },
  setup(props: any, ctx: any) {
    let status = ref(0);

    async function getUserInfo() {

      axios.get('https://jsonplaceholder.typicode.com/users/1').then(res => {
        status.value = 1
        }).catch(err => {
        status.value = 2
      })


    }

    return {
      status,
      getUserInfo
    }
  }
})
</script>

<style scoped>
.read-the-docs {
  color: #888;
}
</style>

```

```js
// 测试代码
import { shallowMount, mount } from "@vue/test-utils";
import { describe, expect, test } from "@jest/globals";
import axios from "axios";

import HelloWorld from "@/components/HelloWorld.vue";
import * as  flushPromises  from "flush-promises";

// var scheduler = typeof setImmediate === 'function' ? setImmediate : setTimeout;

// function flushPromises() {
//   return new Promise(function(resolve) {
//     scheduler(resolve);
//   });
// }
jest.mock("axios");

const mockAxios = axios as jest.Mocked<typeof axios>;

describe("HelloWorld.vue", () => {
  it.only("renders props.msg when passed", async () => {
    const msg = "new message";
    const wrapper = shallowMount(HelloWorld, {
      props: { msg },
    });


    mockAxios.get.mockResolvedValue({ username: "张三" } as any);
    expect(wrapper.find('.loading').exists()).toBeTruthy();
    await wrapper.get("button").trigger("click");
    expect(wrapper.find('.loading').exists()).toBeFalsy();
    expect(mockAxios.get).toHaveBeenCalled();
    // console.log(wrapper.find('.loading'));
    expect(wrapper.find('.success').exists()).toBeTruthy();

    await flushPromises;
    // expect(wrapper.find(".loading").exists()).toBeFalsy();

  });
});



```

- jest 钩子函数
  - beforeAll 所有的 case 前共同运行的的代码。只会运行一次。
  - afterAll
  - beforeEach 所有的 case 前共同运行的的代码。每个 case 都会运行一次。
  - afterEach

```js
const msg = "";
const wrapper = null;
describe("HelloWorld.vue", () => {
  beforeAll(() => {
    // 组件只用实例化一次
    msg = "new message";
    wrapper = shallowMount(HelloWorld, {
      props: { msg },
    });
  });
  beforeEach(() => {
    // get的调用次数会累加，在每个case中断言get的次数时，其实是前面调用的总和。这可不行。
    mockAxios.get.mockReset(); // 每个case 前都重置一下get的调用次数。
  });
  it("renders props.msg when passed", async () => {});
});
```

::: wrapper
如果•
一个测试失败，要注意的是

- 它是否是唯一在运行的测试，使用 only 进行隔离
- 使用 beforeEach 或者 afterEach 清空一些共享状态
  :::

skip 跳过一个 case

### mock 全局组件

演示，不模拟全局组件

```js
// helloWorld.vue

<template>
  <a-button class="btn">btn</a-button>>
</template>
<script lang="ts">
import { ref, defineComponent } from 'vue'
export default defineComponent({
  props: ['msg'],
  components: {
  },
  setup() {}
})
</script>

// 测试文件

import { shallowMount, mount } from "@vue/test-utils";
import { describe, expect, test } from "@jest/globals";
import HelloWorld from "@/components/HelloWorld.vue";

describe("HelloWorld.vue", () => {
  it.only("renders props.msg when passed", async () => {
    const msg = "new message";
    const wrapper = shallowMount(HelloWorld, {
      props: { msg },
    });
    expect(wrapper.get('.btn').text()).toBe('btn')
  });
});


// 测试时会有如下警告

ts-jest[backports] (WARN) Your Jest configuration is outdated. Use the CLI to help migrating it: ts-jest config:migrate <config-file>.
  console.warn
    [Vue warn]: Failed to resolve component: a-button
    If this is a native custom element, make sure to exclude it from component resolution via compilerOptions.isCustomElement.
      at <Anonymous msg="new message" ref="VTU_COMPONENT" >
      at <VTUROOT>
```

```js
// mock 模拟全局组件
import { shallowMount, mount } from "@vue/test-utils";
import { describe, expect, test } from "@jest/globals";
import HelloWorld from "@/components/HelloWorld.vue";

const mockComponent = {
  template: "<div><slot></slot><div>",
};

const globalComponent = {
  "a-button": mockComponent,
  // 'a-button-stub': mockComponent,
};
describe("HelloWorld.vue", () => {
  it.only("renders props.msg when passed", async () => {
    const msg = "new message";
    const wrapper = mount(HelloWorld, {
      props: { msg },
      global: {
        components: globalComponent,
      },
    });
    console.log(wrapper.html());
    // expect(wrapper.get('.btn').text()).toBe('btn')
  });
});
```

### 测试第三方库

- mock 进行测试

```js
// HelloWorld.vue
<template>
  <button @click="delUserInfo" class="btn">delete</button>
</template>
<script lang="ts">
import { ref, defineComponent } from 'vue';
import { message } from 'ant-design-vue';
export default defineComponent({
  props: ['msg'],
  components: {
  },
  setup() {
    const delUserInfo = () => {
      message.success('删除成功')
    }
    return {
      delUserInfo
    }
  }
})
</script>

```

```js
// 测试文件
import { shallowMount, mount } from "@vue/test-utils";
import { describe, expect, test } from "@jest/globals";
import { message } from "ant-design-vue";

import HelloWorld from "@/components/HelloWorld.vue";

jest.mock("ant-design-vue", () => ({
  message: {
    success: jest.fn(),
  },
}));

const mockComponent = {
  template: "<div><slot></slot><div>",
};

describe("HelloWorld.vue", () => {
  it.only("renders props.msg when passed", async () => {
    const msg = "new message";
    const wrapper = mount(HelloWorld, {
      props: { msg },
    });
    await wrapper.get(".btn").trigger("click");
    expect(message.success).toHaveBeenCalled();
    // expect(wrapper.get('.btn').text()).toBe('btn')
  });
});
```

- 真实调用第三方插件

```js
// HelloWorld.vue

<template>
  <!-- <button @click="delUserInfo" class="btn">delete{{ $store.state.count }}</button> -->
  <button @click="delUserInfo" class="btn">delete{{count }}</button>
</template>
<script lang="ts">
import { ref, defineComponent, computed } from 'vue';
import { message } from 'ant-design-vue';
import { useStore } from 'vuex'
export default defineComponent({
  props: ['msg'],
  components: {
  },
  setup() {
    const store = useStore();
    const count = computed(() => store.state.count)
    const delUserInfo = () => {
      store.commit('setCount');
    }
    return {
      delUserInfo,
      count
    }
  }
})
</script>
```

```js
// store/index.js
import { createStore } from "vuex";

// 创建一个新的 store 实例
const store = createStore({
  state() {
    return {
      count: 0,
    };
  },
  mutations: {
    setCount(state) {
      state.count++;
    },
  },
});

export default store;
```

```js
// 测试文件

import { shallowMount, mount } from "@vue/test-utils";
import { describe, expect, test } from "@jest/globals";
import HelloWorld from "@/components/HelloWorld.vue";
import store from "../../src/store/index";

describe("HelloWorld.vue", () => {
  beforeAll(() => {}),
    it("renders props.msg when passed", async () => {
      const msg = "new message";
      const wrapper = mount(HelloWorld, {
        props: { msg },
        global: {
          provide: {
            store,
          },
        },
      });
      await wrapper.get(".btn").trigger("click");
      console.log(store);
      expect(store.state.count).toBe(1);
      // expect(wrapper.get('.btn').text()).toBe('btn')
    });
});
```

- 测试 router

```js
// HelloWorld.vue

<template>
  <!-- <button @click="delUserInfo" class="btn">delete{{ $store.state.count }}</button> -->
  <button @click="delUserInfo" class="btn">delete</button>
</template>
<script lang="ts">
import { ref, defineComponent, computed } from 'vue';
import { useRouter } from 'vue-router';
export default defineComponent({
  props: ['msg'],
  components: {
  },
  setup() {
    const router = useRouter();
    const delUserInfo = () => {
      router.push({ name: 'a' })
    }
    return {
      delUserInfo,
    }
  }
})
</script>

```

```js
// 测试文件
import { shallowMount, mount } from "@vue/test-utils";
import { describe, expect, test } from "@jest/globals";
import HelloWorld from "@/components/HelloWorld.vue";

const mockRouters: string[] = [];
jest.mock("vue-router", () => ({
  useRouter: () => ({
    push: (url: string) => mockRouters.push(url),
  }),
}));
describe("HelloWorld.vue", () => {
  it("renders props.msg when passed", async () => {
    const msg = "new message";
    const wrapper = mount(HelloWorld, {
      props: { msg },
      global: {},
    });
    await wrapper.get(".btn").trigger("click");

    expect(mockRouters[0]).toEqual({ name: "a" });
  });
});
```

### 覆盖率指标

覆盖率命令

```json
"test:unit": "vue-cli-service test:unit --coverage",
"test": "jest --coverage"
```

- %stmts 是语句覆盖率（statement coverage）：是不是每个语句都执行了？
- %Branch 分支覆盖率（branch coverage）：是不是每个 if 代码块都执行了？
- %Funcs 函数覆盖率（function coverage）：是不是每个函数都调用了？
- %Lines 行覆盖率（line coverage）：是不是每一行都执行了？

配置好后，会生成一个 coverage 文件夹  
 三种颜色分别代表不同比例的覆盖率（<50%红色，50%~80%灰色， ≥80%绿色）

推荐测试覆盖率标准 `80%以上`

## TDD

Test-Driven Development，是一种不同于传统软件开发流程的新型的开发方法。它要求在编写某个功能的代码之前先编写测试代码，然后只编
写使测试通过的功能代码，通过测试来推动整个开发的进行。这有助于编写简洁可用和高质量的代码，并加速开发过程。

Test Driven Development -测试驱动开发

- 先根据需求写测试用例
- 测试用例全部失败
- 开始写代码实现
- 将测试用例由失败变成通过

## BDD

## 参考

[jest 官网](https://jestjs.io/)
[Vitest](https://cn.vitest.dev/)
[vue3 jest 单元测试环境搭建](https://zhuanlan.zhihu.com/p/377566681)

## 最佳实践

- 测试与测试之问应该互相不依赖，不依赖顺序
  真正原因：混淆了单元测试和集成测试，导致单元测试中有太多 Moek！如，需要服务器启动才能执行的代码，就不是单元測试了。
  单元测试是针对一段逻辑，有 if……else for 逻辑的，平铺直叙的代码不用测试  
  如果单元没有被拆分，则拆分出来，再做单元测试。正好，这也是非常好的代码重构。  
  摘抄一段《聊聊架构》里的代码

```java
// 改造之前的。不容易进行单元测试，因为有request，需要启动 http服务，单元测试就需要 Mock
public OrderDTO getUserOrder (HttpRequest request) {
  String userId = request.getParameter("userId");
  String orderId = request.getParameter("orderId");
  UserDTO user = userManager.getUser(userId);
  OrderDTO order = orderManager.getOrder(orderId);
  if (order != null && order.getUserId != null && order.getUserId.equals (userId)) {
    order.setUser(user);
    return order;
  }
  return null;
}

// 改造之后

//平铺直叙的代码不需要单元测试
public OrderDTO getUserOrder (HttpRequest request) {
  String userId = request.getParameter("userId");
  String orderId = request.getParameter("orderId");
  UserDTO user = userManager.getUser(userId);
  OrderDTO order = orderManager.getOrder(orderId);
  return checkUser(order, user, userId);
}
// 1/逻辑单元，单独抽离出来，测试输入输出即可，不用 Mock
public OrderDTO checkUser (OrderDTO order, UserDTO user, String userId) {
   if (order != null && order.getUserId != null && order.getUserId.equals (userId)) {
    order.setUser(user);
    return order;
  }
  return null;
}
```

另外，测试覆盖率，应该看抽离出来的单元模块，而非所有代码的。因为有些代码根本就不需要、或者不适合单元测试。

另一个现状

## supertest 接口测试

jest + supertest

```

```

## 压测
